﻿using System.Globalization;
using System.Net;
using Devonline.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;

namespace Devonline.AspNetCore;

/// <summary>
/// 附件及文件操作类服务
/// </summary>
/// <typeparam name="TDbContext">数据库上下文</typeparam>
/// <typeparam name="TAttachment">附件</typeparam>
/// <typeparam name="TKey">键类型</typeparam>
public class AttachmentService<TDbContext, TAttachment, TKey> :
    ServiceBase<TDbContext, TKey>,
    IAttachmentService<TDbContext, TAttachment, TKey>,
    IServiceBase<TDbContext, TKey>
    where TDbContext : DbContext
    where TAttachment : class, IAttachment<TKey>, new()
    where TKey : IConvertible
{
    protected readonly DbSet<TAttachment> _dbSetAttachment;
    public AttachmentService(
        TDbContext context,
        ILogger<AttachmentService<TDbContext, TAttachment, TKey>> logger,
        IDistributedCache cache,
        IHttpContextAccessor httpContextAccessor,
        AppSetting appSetting) :
        base(context, logger, cache, httpContextAccessor, appSetting)
    {
        _dbSetAttachment = _context.Set<TAttachment>();
    }

    #region 业务附件类操作方法
    /// <summary>
    /// 文件下载, 提供类似于文件夹目录结构的访问方式进行下载
    /// 文件路径只支持一级路径, 适用于访问自动保存的文件或者符合目录规范的文件
    /// 形如 2020-01-01/xxx.jpg
    /// </summary>
    /// <typeparam name="TAttachment">附件</typeparam>
    /// <param name="fileName">文件名</param>
    /// <param name="filePath">文件路径(只支持一级路径)</param>
    /// <returns></returns>
    public virtual async Task<TAttachment?> GetAttachmentAsync(string? fileName, string? filePath = default)
    {
        fileName ??= _request.GetRequestOption<string>(nameof(fileName));
        filePath ??= _request.GetRequestOption<string>(nameof(filePath));
        if (string.IsNullOrWhiteSpace(fileName))
        {
            _logger.LogError("缺少必须的文件名!");
            throw new ArgumentNullException(fileName, "缺少必须的文件名!");
        }

        _logger.LogInformation("User {user} get the file from " + $"path <{filePath}> and name <{fileName}>", UserName);
        filePath = string.IsNullOrWhiteSpace(filePath) ? fileName : Path.Combine(filePath, fileName);
        return await GetQueryable<TAttachment>().FirstOrDefaultAsync(x => x.Path == filePath).ConfigureAwait(false);
    }
    /// <summary>
    /// 获取附件对应的文件, 适用于文件下载
    /// </summary>
    /// <typeparam name="TAttachment">附件</typeparam>
    /// <param name="fileName"></param>
    /// <param name="filePath"></param>
    /// <returns></returns>
    /// <exception cref="BadHttpRequestException"></exception>
    public virtual async Task<IActionResult> GetAttachmentFileAsync(string? fileName, string? filePath = default)
    {
        var attachment = await GetAttachmentAsync(fileName, filePath);
        if (attachment == null || string.IsNullOrWhiteSpace(attachment.Path))
        {
            _logger.LogError($"File {filePath}/{fileName} not found!");
            return new NotFoundResult();
        }

        filePath = GetAttachmentPath(attachment.Path);
        if (!File.Exists(filePath))
        {
            _logger.LogError($"File {filePath}/{fileName} not found!");
            return new NotFoundResult();
        }

        _logger.LogInformation("User {user} get the file from " + $"path <{filePath}> and name <{fileName}> success", UserName);
        return new PhysicalFileResult(filePath, attachment.ContentType ?? ContentType.Default) { FileDownloadName = attachment.Name ?? fileName };
    }
    /// <summary>
    /// 获取文件, 按业务类型和业务主键获取
    /// </summary>
    /// <typeparam name="TAttachment">附件</typeparam>
    /// <param name="businessType">业务类型</param>
    /// <param name="businessKey">业务主键</param>
    /// <returns></returns>
    public virtual async Task<IEnumerable<TAttachment>?> GetAttachmentsAsync(string? businessType = default, TKey? businessKey = default)
    {
        businessType ??= _request.GetRequestOption<string>(nameof(businessType));
        businessKey ??= _request.GetRequestOption<TKey>(nameof(businessKey));
        _logger.LogInformation("User {user} query the attachment list with businessType {businessType} " + $"and businessKey {businessKey}", UserName, businessType);
        if (businessKey == null || businessKey.Equals(default))
        {
            throw new BadHttpRequestException("必须提供业务相关参数!");
        }

        var queryable = GetQueryable<TAttachment>();
        if (!string.IsNullOrWhiteSpace(businessType))
        {
            queryable = queryable.Where(x => x.BusinessType == businessType);
        }

        if (!(businessKey == null || businessKey.Equals(default)))
        {
            queryable = queryable.Where(x => x.BusinessKey != null && x.BusinessKey.Equals(businessKey));
        }

        var attachments = await queryable.ToListAsync();
        _logger.LogInformation("User {user} query the attachment list with businessType {businessType} " + $"and businessKey {businessKey} success", UserName, businessType);
        return attachments;
    }

    /// <summary>
    /// 单个文件上传
    /// </summary>
    /// <typeparam name="TAttachment">附件</typeparam>
    /// <param name="file"></param>
    /// <returns></returns>
    public virtual async Task<UploadResult<TAttachment, TKey>> UploadAsync(IFormFile? file = default)
    {
        if (file == null && _request.HasFormContentType && _request.Form.Files.Any())
        {
            file = _request.Form.Files[0];
        }

        if (file == null)
        {
            throw new BadHttpRequestException("没有提交任何文件!");
        }

        var uploadResult = new UploadResult<TAttachment, TKey>
        {
            FileName = file.FileName,
            StatusCode = HttpStatusCode.NotAcceptable
        };

        if (file.Length <= 0)
        {
            uploadResult.Result = "文件内容为空!";
            return uploadResult;
        }

        if (file.Length > MaxUploadFileSize)
        {
            uploadResult.Result = $"文件 {file.Name} 大小超出 {MaxUploadFileSize} 单文件最大上传限制!";
            return uploadResult;
        }

        var ext = Path.GetExtension(file.FileName);
        var extUpper = ext.ToUpperInvariant();
        if (!AllowFileExtensions.Any(x => x == extUpper))
        {
            uploadResult.Result = "文件类型禁止上传!";
            return uploadResult;
        }

        var attachment = new TAttachment()
        {
            Name = file.FileName,
            Length = file.Length,
            Extension = ext,
            ContentType = file.ContentType
        };

        var attachmentPath = DateTime.UtcNow.ToString(AppSettings.DEFAULT_DATE_FORMAT, CultureInfo.InvariantCulture);
        var attachmentFile = GetAttachmentPath(attachmentPath);
        if (!Directory.Exists(attachmentFile))
        {
            Directory.CreateDirectory(attachmentFile);
        }

        var fileName = KeyGenerator.GetKey() + ext;
        attachment.Path = Path.Combine(attachmentPath, fileName);
        fileName = Path.Combine(attachmentFile, fileName);

        try
        {
            using (var stream = new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.Read))
            {
                await file.CopyToAsync(stream).ConfigureAwait(false);
                stream.Close();
            }

            uploadResult.StatusCode = HttpStatusCode.OK;
            uploadResult.Result = attachment.Path;
            uploadResult.Attachment = attachment;

            _logger.LogInformation("User {user} upload the " + $"file <{uploadResult.FileName}> success with result <{uploadResult.Result}>!", UserName);

            #region 图片文件创建缩略图
            if ((_appSetting.Attachment?.EnableImageThumbnail ?? false) && attachment.ContentType.ToUpperInvariant().StartsWith(nameof(SixLabors.ImageSharp.Image).ToUpperInvariant(), StringComparison.InvariantCultureIgnoreCase))
            {
                await fileName.CreateThumbnailAsync(ImageThumbnailSize, ImageThumbnailSize, Path.Combine(attachmentFile, ImageThumbnailPrefix + Path.GetFileName(fileName)));
                _logger.LogInformation("User {user} upload the " + $"file <{uploadResult.FileName}> and create image thumbnail success with result <{uploadResult.Result}>!", UserName);
            }
            #endregion
        }
        catch (Exception ex)
        {
            uploadResult.StatusCode = HttpStatusCode.NotAcceptable;
            uploadResult.Result = ex.Message;
            _logger.LogError(ex, "User {user} upload the " + $"file <{uploadResult.FileName}> fail with error: <{ex.GetMessage()}>!", UserName);
        }

        return uploadResult;
    }
    /// <summary>
    /// 批量文件上传
    /// </summary>
    /// <typeparam name="TAttachment">附件</typeparam>
    /// <param name="files"></param>
    /// <returns></returns>
    public virtual async Task<IEnumerable<UploadResult<TAttachment, TKey>>> UploadAsync(IEnumerable<IFormFile>? files = default)
    {
        if ((files == null || (!files.Any())) && _request.HasFormContentType && _request.Form.Files.IsNotNullOrEmpty())
        {
            files = _request.Form.Files;
        }

        if (files == null || (!files.Any()) || files.Sum(x => x.Length) <= 0)
        {
            throw new BadHttpRequestException("没有提交任何文件!");
        }

        if (files.Sum(x => x.Length) > TotalUploadFileSize)
        {
            throw new BadHttpRequestException($"文件总大小超出 {TotalUploadFileSize} 最大上传限制!");
        }

        var filePath = DateTime.UtcNow.ToString(AppSettings.DEFAULT_DATE_FORMAT, CultureInfo.InvariantCulture);
        var attachmentPath = GetAttachmentPath(filePath);
        if (!Directory.Exists(attachmentPath))
        {
            Directory.CreateDirectory(attachmentPath);
        }

        var uploadResults = new List<UploadResult<TAttachment, TKey>>();
        var allowFileExtensions = (_appSetting.Attachment?.AllowFileExtensions ?? AppSettings.DEFAULT_ALLOW_FILE_EXTENSIONS).Split(AppSettings.CHAR_COMMA, StringSplitOptions.RemoveEmptyEntries);
        foreach (var file in files)
        {
            uploadResults.Add(await UploadAsync(file));
        }

        if (uploadResults.Count > 0)
        {
            //日志记录本次上传最终结果: 任意一个文件上传成功算成功
            var fileNames = uploadResults.Select(x => x.FileName ?? string.Empty).ToString<string>();
            var results = uploadResults.Select(x => x.Result ?? string.Empty).ToString<string>();
            if (uploadResults.Any(x => x.StatusCode == HttpStatusCode.OK))
            {
                _logger.LogInformation("User {user} upload " + $"the files <{fileNames}> success with result <{results}>!", UserName);
            }
            else
            {
                _logger.LogWarning("User {user} upload " + $"the files <{fileNames}> success with result <{results}>!", UserName);
            }
        }

        return uploadResults;
    }

    /// <summary>
    /// 新增附件
    /// </summary>
    /// <typeparam name="TEntitySet">业务数据类型</typeparam>
    /// <param name="entitySet">业务数据</param>
    /// <param name="attachments">附件集合</param>
    /// <param name="businessType">业务类型</param>
    /// <returns>待删除文件列表, 请在数据库事物提交成功后删除列表中的文件</returns>
    public virtual async Task AddAttachmentsAsync<TEntitySet>(TEntitySet entitySet, IEnumerable<TAttachment>? attachments = default, string? businessType = default) where TEntitySet : class, IEntitySet<TKey>
    {
        attachments ??= GetCollectionMembers<TEntitySet, TAttachment>(entitySet);
        if (attachments == null || !attachments.Any())
        {
            return;
        }

        businessType ??= GetBusinessType<TEntitySet>();
        foreach (var attachment in attachments)
        {
            attachment.BusinessKey ??= entitySet.Id;
            attachment.BusinessType ??= businessType;
            Create(attachment);
            Detach(attachment);
            await _dbSetAttachment.AddAsync(attachment);
        }

        SetBusinessAttachmentPath(entitySet, attachments);
    }
    /// <summary>
    /// 修改附件, 保留旧附件, 新增新附件, 返回待删除文件列表
    /// </summary>
    /// <typeparam name="TEntitySet">业务数据类型</typeparam>
    /// <param name="entitySet">业务数据</param>
    /// <param name="attachments">附件集合</param>
    /// <param name="businessType">业务类型</param>
    /// <param name="isLogical">是否逻辑操作, 默认不是</param>
    /// <returns>待删除文件列表, 请在数据库事物提交成功后删除列表中的文件</returns>
    public virtual async Task<IEnumerable<string>> UpdateAttachmentsAsync<TEntitySet>(TEntitySet entitySet, IEnumerable<TAttachment>? attachments = default, string? businessType = default, bool isLogical = false) where TEntitySet : class, IEntitySet<TKey>
    {
        businessType ??= GetBusinessType<TEntitySet>();
        var news = attachments ?? GetCollectionMembers<TEntitySet, TAttachment>(entitySet) ?? new List<TAttachment>();
        var olds = await GetQueryable<TAttachment>().Where(x => x.BusinessType == businessType && x.BusinessKey != null && x.BusinessKey.Equals(entitySet.Id)).ToListAsync() ?? new List<TAttachment>();
        foreach (var attachment in news)
        {
            if (!olds.Any(x => x.Path == attachment.Path || x.Id.Equals(attachment.Id)))
            {
                //新的有旧的没有, 则新增, 附件只有新增和删除, 没有修改的说法
                attachment.BusinessKey ??= entitySet.Id;
                attachment.BusinessType ??= businessType;
                Create(attachment);
                Detach(attachment);
                _dbSetAttachment.Add(attachment);
            }
        }

        var files = new List<string>();
        foreach (var attachment in olds)
        {
            //旧的有新的没有则删除
            //找 Id 或者 Path 值相等的, 新的里面找不到则删除旧的
            if ((!string.IsNullOrWhiteSpace(attachment.Path)) && (!news.Any(x => x.Id.Equals(attachment.Id) || x.Path == attachment.Path)))
            {
                files.Add(attachment.Path);
                Delete(attachment, isLogical);
            }
        }

        SetBusinessAttachmentPath(entitySet, attachments);
        return files;
    }
    /// <summary>
    /// 自动删除附件, 返回待删除文件列表, 适用于实体对象模型从数据库删除时查找原始附件并自动删除
    /// </summary>
    /// <param name="businessKey">业务主键</param>
    /// <param name="businessType">业务类型</param>
    /// <param name="isLogical">是否逻辑操作, 默认不是</param>
    /// <returns>待删除文件列表, 请在数据库事物提交成功后删除列表中的文件</returns>
    public virtual async Task<IEnumerable<string>?> DeleteAttachmentsAsync(TKey businessKey, string? businessType = default, bool isLogical = false)
    {
        var queryable = GetQueryable<TAttachment>();
        if (businessKey != null && !businessKey.Equals(default))
        {
            queryable = queryable.Where(x => x.BusinessKey != null && x.BusinessKey.Equals(businessKey));
        }

        if (!string.IsNullOrWhiteSpace(businessType))
        {
            queryable = queryable.Where(x => x.BusinessType == businessType);
        }

        var attachments = await queryable.ToListAsync();
        if (attachments.Count < 1)
        {
            return null;
        }

        var files = attachments.Select(x => x.Path ?? string.Empty).Distinct();
        foreach (var attachment in attachments)
        {
            Delete(attachment, isLogical);
        }

        return files;
    }
    /// <summary>
    /// 按文件列表删除附件
    /// </summary>
    /// <param name="fileNames">待删除的文件名</param>
    /// <param name="isLogical">是否逻辑操作, 默认不是</param>
    /// <returns></returns>
    public virtual async Task<IEnumerable<string>?> DeleteAttachmentsAsync(IEnumerable<string> fileNames, bool isLogical = false)
    {
        var attachments = await GetQueryable<TAttachment>().Where(x => x.Path != null && fileNames.Contains(x.Path)).ToListAsync();
        if (attachments.Count < 1)
        {
            return null;
        }

        var files = attachments.Select(x => x.Path ?? string.Empty).Distinct();
        foreach (var attachment in attachments)
        {
            Delete(attachment, isLogical);
        }

        return files;
    }
    #endregion
}

/// <summary>
/// 附件及文件操作类服务
/// </summary>
/// <typeparam name="TDbContext">数据库上下文</typeparam>
public class AttachmentService<TDbContext> :
    AttachmentService<TDbContext, Attachment, string>,
    IAttachmentService<TDbContext>,
    IServiceBase<TDbContext>
    where TDbContext : DbContext
{
    public AttachmentService(
        TDbContext context,
        ILogger<AttachmentService<TDbContext>> logger,
        IDistributedCache cache,
        IHttpContextAccessor httpContextAccessor,
        AppSetting appSetting) :
        base(context, logger, cache, httpContextAccessor, appSetting)
    { }
}